package com.clei.netty.file.server;

import com.clei.consts.NettyConstants;
import com.clei.netty.file.dto.MsgBody;
import com.clei.netty.file.enums.FileCmdTypeEnum;
import com.clei.netty.util.FileDataManager;
import com.clei.netty.util.FilePathManager;
import com.clei.utils.ByteUtil;
import com.clei.utils.FileUtil;
import com.clei.utils.PrintUtil;
import com.clei.utils.helper.BytesReader;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.io.File;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

/**
 * file server handler
 *
 * @author Y
 * @date 2022-04-21
 */
public class FileServerHandler extends SimpleChannelInboundHandler<MsgBody> {

    /**
     * channel group
     */
    private static final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 文件id计数器
     */
    private static final AtomicInteger FILE_ID_COUNTER = new AtomicInteger();

    /**
     * fileName -> filePath
     */
    private static final Map<String, String> FILE_NAME_PATH = new ConcurrentHashMap<>();

    /**
     * fileName -> fileSize
     */
    private static final Map<String, Long> FILE_NAME_SIZE = new ConcurrentHashMap<>();

    /**
     * client -> sqlCounter
     */
    private static final Map<Integer, AtomicInteger> CLIENT_SEQ_COUNTER = new ConcurrentHashMap<>();

    /**
     * 文件id计数器文件后缀
     */
    private static final String ID_COUNTER_FILE_SUFFIX = ".ID_COUNTER";
    /**
     * 默认的文件存放文件夹
     */
    private static final String FILE_DIRECTORY = "E:\\netty\\file\\";
    /**
     * 文件上传size限制 10G
     */
    private static final long FILE_SIZE_LIMIT = 10L * 1024 * 1024 * 1024;
    /**
     * 文件id计数器文件
     */
    private static File idCounterFile = null;

    public FileServerHandler() {
        try {
            // 目录创建 计数器初始化
            File directory = new File(FILE_DIRECTORY);
            if (!directory.exists()) {
                directory.mkdirs();
                idCounterFile = new File(FILE_DIRECTORY + "0" + ID_COUNTER_FILE_SUFFIX);
                idCounterFile.createNewFile();
            } else {
                File[] files = directory.listFiles();
                Optional<String> any = Arrays.stream(files)
                        .map(File::getName)
                        .filter(n -> n.endsWith(ID_COUNTER_FILE_SUFFIX))
                        .findAny();
                if (any.isPresent()) {
                    idCounterFile = new File(FILE_DIRECTORY + any.get());
                    FILE_ID_COUNTER.addAndGet(Integer.parseInt(any.get().replace(ID_COUNTER_FILE_SUFFIX, "")));
                } else {
                    idCounterFile = new File(FILE_DIRECTORY + "0" + ID_COUNTER_FILE_SUFFIX);
                    idCounterFile.createNewFile();
                }
                for (File f : files) {
                    FILE_NAME_SIZE.put(f.getName(), f.length());
                    FILE_NAME_PATH.put(f.getName(), f.getAbsolutePath());
                }
                FILE_NAME_SIZE.remove(idCounterFile.getName());
                FILE_NAME_PATH.remove(idCounterFile.getName());
            }
        } catch (Exception e) {
            throw new RuntimeException("创建FileServerHandler出错", e);
        }
    }

    /**
     * 有新连接加入
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        PrintUtil.log("[{}]handlerAdded", ctx.channel().remoteAddress());
        Channel client = ctx.channel();
        // 添加到组内
        CHANNEL_GROUP.add(client);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        PrintUtil.log("[{}]handlerRemoved", ctx.channel().remoteAddress());
        Channel client = ctx.channel();
        // 关闭的channel会自动从channelGroup remove
        // 无需调用channelGroup.remove(client)
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        PrintUtil.log("[{}]channelActive", ctx.channel().remoteAddress());
        super.channelActive(ctx);
        // 初始化计数器
        CLIENT_SEQ_COUNTER.put(getClientId(ctx), new AtomicInteger());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        PrintUtil.log("[{}]channelInactive", ctx.channel().remoteAddress());
        super.channelInactive(ctx);
        // 移除计数器
        CLIENT_SEQ_COUNTER.remove(getClientId(ctx));
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        PrintUtil.log("[{}]channelRegistered", ctx.channel().remoteAddress());
        super.channelRegistered(ctx);
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        PrintUtil.log("[{}]channelUnregistered", ctx.channel().remoteAddress());
        super.channelUnregistered(ctx);
    }

    /**
     * 广播消息
     *
     * @param ctx  不发送消息的频道
     * @param cmd  cmd
     * @param data data
     */
    public static void broadcast(ChannelHandlerContext ctx, byte cmd, byte[] data) {
        boolean filter = null != ctx;
        for (Channel channel : CHANNEL_GROUP) {
            if (filter && channel != ctx.channel()) {
                channel.writeAndFlush(new MsgBody(getSeq(channel), 0, cmd, data));
            }
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        PrintUtil.log("[{}]异常关闭", ctx.channel().remoteAddress(), cause);
        // 遇到异常就关闭连接
        ctx.close();
    }

    /**
     * 获取用户id
     *
     * @param ctx 频道上下文
     * @return 用户id
     */
    private static Integer getClientId(ChannelHandlerContext ctx) {
        return getClientId(ctx.channel());
    }

    /**
     * 获取用户id
     *
     * @param channel 频道
     * @return 用户id
     */
    private static Integer getClientId(Channel channel) {
        return channel.hashCode() + channel.remoteAddress().toString().hashCode();
    }

    /**
     * 获取序号
     *
     * @param ctx ctx 频道上下文
     * @return seq
     */
    private static int getSeq(ChannelHandlerContext ctx) {
        return getSeq(ctx.channel());
    }

    /**
     * 获取序号
     *
     * @param channel 频道
     * @return seq
     */
    private static int getSeq(Channel channel) {
        Integer clientId = getClientId(channel);
        AtomicInteger seqCounter = CLIENT_SEQ_COUNTER.get(clientId);
        return seqCounter.incrementAndGet();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgBody msg) {
        byte cmd = msg.getCmd();
        FileCmdTypeEnum cmdType = FileCmdTypeEnum.getByCmdCode(cmd);
        try {
            if (!FileCmdTypeEnum.REQ_UPLOAD_DATA.equals(cmdType)) {
                PrintUtil.log("address : {}, msg : {}", ctx.channel().remoteAddress(), msg);
            }
            switch (cmdType) {
                case REQ_UPLOAD:
                    handleUpload(ctx, msg);
                    break;
                case REQ_UPLOAD_DATA:
                    handleUploadData(msg);
                    break;
                case REQ_CATEGORY:
                    sendMsg(ctx, msg.getSeq(), FileCmdTypeEnum.RES_CATEGORY.getCmdCode(), getFileCategory());
                    break;
                case REQ_DOWNLOAD:
                    handleDownload(ctx, msg);
                    break;
                case REQ_CAPTURE_SCREEN:
                    handleCaptureScreen(ctx, msg);
                    break;
                case REQ_CMD:
                    handleCmd(ctx, msg);
                    break;
                default:
            }
        } catch (Exception e) {
            PrintUtil.log("请求处理出错 address : {}, msg : {},", ctx.channel().remoteAddress(), msg, e);
        }
    }

    /**
     * 发送消息
     *
     * @param ctx  ctx 频道上下文
     * @param ack  回应 对应序号 从1开始
     * @param cmd  命令类型
     * @param data 数据
     * @see FileCmdTypeEnum
     */
    private void sendMsg(ChannelHandlerContext ctx, int ack, byte cmd, byte[] data) {
        ctx.channel().writeAndFlush(new MsgBody(getSeq(ctx), ack, cmd, data));
    }

    /**
     * 判断文件是否能上传
     *
     * @param ctx      ctx 频道上下文
     * @param msg      消息体
     * @param fileName 文件名称
     * @param fileSize 文件大小
     * @return 是否可上传
     */
    private boolean canUpload(ChannelHandlerContext ctx, MsgBody msg, String fileName, long fileSize) {
        FileCmdTypeEnum res = null;
        String resMsg = null;
        if (FILE_NAME_SIZE.containsKey(fileName)) {
            res = FileCmdTypeEnum.RES_UPLOAD_REFUSE;
            resMsg = "文件已存在！";
        } else if (fileSize > FILE_SIZE_LIMIT) {
            res = FileCmdTypeEnum.RES_UPLOAD_REFUSE;
            resMsg = "文件大小超过限制！ limit : " + FILE_SIZE_LIMIT;
        }
        if (null != res) {
            sendMsg(ctx, msg.getSeq(), res.getCmdCode(), ByteUtil.toBytes(fileName));
            PrintUtil.log("cmd : {}, fileName : {}, res : {}", res.getCmdName(), fileName, resMsg);
            return false;
        } else {
            return true;
        }
    }

    /**
     * 更新文件数据
     *
     * @param ctx      ctx 频道上下文
     * @param msg      消息体
     * @param fileName 文件名
     * @param fileSize 文件大小
     * @return 文件id
     */
    private synchronized int updateFileData(ChannelHandlerContext ctx, MsgBody msg, String fileName, long fileSize) {
        int fileId = FILE_ID_COUNTER.incrementAndGet();
        FILE_NAME_SIZE.put(fileName, fileSize);
        FILE_NAME_PATH.put(fileName, FILE_DIRECTORY + fileName);
        int capacity = (int) ((fileSize + NettyConstants.FILE_BUFFER_SIZE - 1) / NettyConstants.FILE_BUFFER_SIZE);
        // 文件传输完毕的操作
        Consumer<Integer> callbackHandler = f -> sendMsg(ctx, msg.getSeq(), FileCmdTypeEnum.RES_UPLOAD_SUCCESS.getCmdCode(), ByteUtil.toBytes(fileName));
        FileDataManager.put(fileId, capacity, FILE_DIRECTORY + fileName, false, callbackHandler);
        File newIdCounterFile = new File(FILE_DIRECTORY + fileId + ID_COUNTER_FILE_SUFFIX);
        idCounterFile.renameTo(newIdCounterFile);
        return fileId;
    }

    /**
     * 获取文件目录数据
     *
     * @return 文件目录数据
     */
    private byte[] getFileCategory() {
        Map<String, Long> fileNameSize = new TreeMap<>(FILE_NAME_SIZE);
        int count = fileNameSize.size();
        byte[] res = ByteUtil.intTo4Byte(count);
        for (Map.Entry<String, Long> next : fileNameSize.entrySet()) {
            byte[] fileNameBytes = ByteUtil.toBytes(next.getKey());
            res = ByteUtil.merge(res, ByteUtil.longTo8Byte(next.getValue()), ByteUtil.intTo4Byte(fileNameBytes.length), fileNameBytes);
        }
        return res;
    }

    /**
     * 处理上传请求
     *
     * @param ctx ctx 频道上下文
     * @param msg 消息体
     */
    private void handleUpload(ChannelHandlerContext ctx, MsgBody msg) {
        BytesReader br = new BytesReader(msg.getData());
        long fileSize = br.readLong8();
        String fileName = ByteUtil.toString(br.readLast());
        PrintUtil.log("上传文件 fileName : {}, fileSize : {}", fileName, fileSize);
        // 判断是否能上传
        boolean canUpload = canUpload(ctx, msg, fileName, fileSize);
        if (canUpload) {
            int fileId = updateFileData(ctx, msg, fileName, fileSize);
            FileCmdTypeEnum res = FileCmdTypeEnum.RES_UPLOAD_ACK;
            // 文件id和文件名
            byte[] resData = ByteUtil.merge(ByteUtil.intTo4Byte(fileId), ByteUtil.toBytes(fileName));
            sendMsg(ctx, msg.getSeq(), res.getCmdCode(), resData);
            PrintUtil.log("上传文件 fileName : {}, res : {}", fileName, res.getCmdName());
        }
    }

    /**
     * 处理上传数据
     *
     * @param msg 消息体
     */
    private void handleUploadData(MsgBody msg) {
        BytesReader br = new BytesReader(msg.getData());
        int fileId = br.readInt4();
        int index = br.readInt4();
        FileDataManager.add(fileId, index, br.readLast());
    }

    /**
     * 处理下载请求
     *
     * @param ctx ctx 频道上下文
     * @param msg 消息体
     */
    private void handleDownload(ChannelHandlerContext ctx, MsgBody msg) {
        BytesReader br = new BytesReader(msg.getData());
        int fileId = br.readInt4();
        String fileName = ByteUtil.toString(br.readLast());
        String filePath = FILE_NAME_PATH.get(fileName);
        File file = null == filePath ? null : new File(filePath);
        if (null == filePath || !file.exists()) {
            FileCmdTypeEnum resCmd = FileCmdTypeEnum.RES_DOWNLOAD_REFUSE;
            sendMsg(ctx, msg.getSeq(), resCmd.getCmdCode(), ByteUtil.toBytes(fileName));
            PrintUtil.log("cmd : {}, fileName : {}", resCmd.getCmdName(), fileName);
        } else {
            byte[] fileIdBytes = ByteUtil.intTo4Byte(fileId);
            // 数据 = fileId + index + bytes[]
            byte cmdCode = FileCmdTypeEnum.RES_DOWNLOAD_DATA.getCmdCode();
            FileUtil.bytesHandle(file, NettyConstants.FILE_BUFFER_SIZE,
                    (index, bytes) -> {
                        byte[] indexBytes = ByteUtil.intTo4Byte(index);
                        sendMsg(ctx, msg.getSeq(), cmdCode, ByteUtil.merge(fileIdBytes, indexBytes, bytes));
                    });
            FilePathManager.remove(fileName);
            PrintUtil.log("下载文件数据传输完成 filePath : {}", filePath);
        }
    }

    /**
     * 处理截屏上传请求
     *
     * @param ctx ctx 频道上下文
     * @param msg 源消息
     */
    private void handleCaptureScreen(ChannelHandlerContext ctx, MsgBody msg) {
        broadcast(ctx, msg.getCmd(), NettyConstants.EMPTY);
        sendMsg(ctx, msg.getSeq(), FileCmdTypeEnum.RES_CAPTURE_SCREEN_ACK.getCmdCode(), NettyConstants.EMPTY);
    }

    /**
     * 处理执行命令请求
     *
     * @param ctx ctx 频道上下文
     * @param msg 源消息
     */
    private void handleCmd(ChannelHandlerContext ctx, MsgBody msg) {
        broadcast(ctx, msg.getCmd(), msg.getData());
        sendMsg(ctx, msg.getSeq(), FileCmdTypeEnum.RES_CMD_ACK.getCmdCode(), NettyConstants.EMPTY);
    }
}
